Skip to content

refactor: stop probing emptiness with Object.keys(obj).length - #9548

Merged
BridgeAR merged 11 commits into
masterfrom
BridgeAR/2026-07-27-eslint-object-keys-length
Aug 17, 2026
Merged

refactor: stop probing emptiness with Object.keys(obj).length#9548
BridgeAR merged 11 commits into
masterfrom
BridgeAR/2026-07-27-eslint-object-keys-length

Conversation

@BridgeAR

@BridgeAR BridgeAR commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Object.keys(obj).length allocates the whole key array to answer a yes/no question, and every site removed here
sits on a hot path. Each one now answers the question from a shape that already carries the answer, and a
no-restricted-syntax selector under packages/*/src keeps the probe from growing back.

  1. ritm counts hooked modules as they are registered and removed, so teardown decides from a counter rather than
    from the key set. unhook skips modules it no longer holds, so a repeated teardown cannot undercount and leave
    the require patch installed.
  2. The OTel context manager builds the baggage entries in one pass and lets the absence of that object stand for
    "no baggage". Azure Functions span links drop the probe in front of tracer.extract, which already returns
    null for a carrier without trace context.
  3. Profiling reports absent profiler info as undefined instead of an empty object the submitter has to probe.
    The endpoint-label check went with it: webTags is only ever the tag map of a span isWebServerSpan matched,
    so it always carries span.type and the probe could never fire.
  4. The OpenAI log store is created by whichever extraction helper first writes to it, so store.openai stays
    undefined for the methods that record nothing.
  5. LLMObs tracks metadata presence at the assignment site, because a flag set beside Object.assign would claim
    metadata exists even when the source object is empty.
  6. Test Optimization leaves coverage and the EFD retry bucket map undefined when the backend sends nothing, so
    no caller has to tell an empty object apart from real data.

Why the rule is narrow

The selector only matches probe positions — a comparison against 0, a ! operand, or the test of an if,
ternary, logical expression or loop — so a genuine count such as writeMapPrefix(Object.keys(x).length) stays
legal.

Four sites keep the probe behind an inline opt-out. Fastify cookies and bodies and router path params reach us as
third-party objects with arbitrary keys, where publishing an empty one would set the matching WAF address and burn
the once-per-request publish latch. The debugger snapshot walker faces the same shape: fields is keyed by the
user's variable names.

@dd-octo-sts

dd-octo-sts Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 8.2 MB
Deduped: 8.86 MB
No deduping: 8.86 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.3 | 125.43 kB | 445.14 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Jul 27, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 1 test - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 99.09%
Overall Coverage: 98.53% (-0.03%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 3ee4385 | Docs | Datadog PR Page | Give us feedback!

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.53%. Comparing base (b85f911) to head (3ee4385).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master    #9548    +/-   ##
========================================
  Coverage   98.53%   98.53%            
========================================
  Files         978      978            
  Lines      144176   144181     +5     
  Branches    12738    12496   -242     
========================================
+ Hits       142059   142068     +9     
+ Misses       2117     2113     -4     
Flag Coverage Δ
aiguard 57.45% <11.11%> (+0.19%) ⬆️
aiguard-integration 55.47% <31.50%> (-0.03%) ⬇️
apm-bucket-0 57.62% <11.11%> (-0.04%) ⬇️
apm-bucket-1 62.81% <15.00%> (-0.03%) ⬇️
apm-bucket-2 61.64% <11.11%> (-0.03%) ⬇️
apm-bucket-3 59.27% <11.11%> (-0.03%) ⬇️
apm-capabilities-tracing 62.36% <32.60%> (+0.04%) ⬆️
apm-integrations-aerospike 55.51% <11.11%> (-0.03%) ⬇️
apm-integrations-confluentinc-kafka-javascript 60.63% <11.11%> (-0.04%) ⬇️
apm-integrations-couchbase 56.21% <11.11%> (-0.03%) ⬇️
apm-integrations-http 61.37% <10.00%> (-0.04%) ⬇️
apm-integrations-kafkajs 61.14% <11.11%> (-0.04%) ⬇️
apm-integrations-next 58.88% <11.11%> (-0.03%) ⬇️
apm-integrations-prisma 58.32% <36.00%> (-0.03%) ⬇️
appsec 71.51% <11.11%> (-0.01%) ⬇️
appsec-express_fastify_graphql 68.91% <27.27%> (-0.03%) ⬇️
appsec-integration 49.60% <18.18%> (-0.01%) ⬇️
appsec-kafka_ldapjs_lodash 62.88% <11.11%> (-0.03%) ⬇️
appsec-mongodb-core_mongoose_mysql 66.36% <15.00%> (-0.03%) ⬇️
appsec-next 56.24% <12.90%> (-0.03%) ⬇️
appsec-node-serialize_passport_postgres 65.77% <15.00%> (-0.03%) ⬇️
appsec-sourcing_stripe_template 64.21% <10.00%> (-0.03%) ⬇️
debugger 63.66% <15.00%> (-0.03%) ⬇️
instrumentations-bucket-0 51.24% <11.11%> (-0.03%) ⬇️
instrumentations-bucket-1 59.12% <10.00%> (-0.04%) ⬇️
instrumentations-bucket-10 60.38% <15.00%> (-0.03%) ⬇️
instrumentations-bucket-11 61.04% <15.00%> (-0.03%) ⬇️
instrumentations-bucket-12 51.15% <11.11%> (-0.03%) ⬇️
instrumentations-bucket-13 52.00% <10.00%> (-0.04%) ⬇️
instrumentations-bucket-14 51.25% <11.11%> (-0.03%) ⬇️
instrumentations-bucket-2 52.48% <10.00%> (-0.03%) ⬇️
instrumentations-bucket-3 53.12% <15.00%> (-0.03%) ⬇️
instrumentations-bucket-4 58.21% <22.72%> (-0.03%) ⬇️
instrumentations-bucket-5 48.82% <12.00%> (-0.03%) ⬇️
instrumentations-bucket-6 59.73% <11.11%> (-0.03%) ⬇️
instrumentations-bucket-7 51.44% <11.11%> (-0.03%) ⬇️
instrumentations-bucket-8 57.91% <13.63%> (-0.03%) ⬇️
instrumentations-bucket-9 56.77% <22.22%> (-0.01%) ⬇️
instrumentations-instrumentation-couchbase 50.21% <11.11%> (-0.04%) ⬇️
instrumentations-integration-esbuild 33.89% <13.63%> (-0.01%) ⬇️
llmobs-ai_anthropic_bedrock 62.31% <37.83%> (-0.02%) ⬇️
llmobs-bucket-1 60.82% <28.81%> (-0.04%) ⬇️
llmobs-openai 61.52% <71.66%> (+0.03%) ⬆️
llmobs-openai-agents_vertex-ai 59.54% <45.00%> (-0.03%) ⬇️
llmobs-sdk 67.22% <16.66%> (-0.03%) ⬇️
master-coverage 98.53% <100.00%> (?)
openfeature 55.20% <15.00%> (-0.01%) ⬇️
openfeature-unit 52.83% <11.11%> (-0.03%) ⬇️
platform-core_esbuild_instrumentations-misc 40.85% <13.63%> (-0.02%) ⬇️
platform-integration 59.91% <27.58%> (-0.01%) ⬇️
platform-shimmer_unit-guardrails_webpack 38.50% <13.63%> (-0.02%) ⬇️
plugins-browser-bunyan_bullmq_cassandra 61.02% <11.11%> (-0.03%) ⬇️
plugins-bucket-0 56.42% <11.11%> (-0.03%) ⬇️
plugins-bucket-1 53.54% <15.00%> (-0.01%) ⬇️
plugins-bucket-11 61.38% <15.00%> (-0.03%) ⬇️
plugins-bucket-18 60.95% <10.00%> (-0.03%) ⬇️
plugins-bucket-19 59.19% <11.11%> (-0.03%) ⬇️
plugins-bucket-20 61.10% <11.11%> (-0.03%) ⬇️
plugins-bucket-4 55.82% <11.11%> (-0.04%) ⬇️
plugins-cookie_cookie-parser_crypto 50.77% <11.11%> (-0.04%) ⬇️
plugins-fastify_fetch_fs 60.03% <18.18%> (-0.06%) ⬇️
plugins-generic-pool_google-cloud-pubsub_grpc 63.58% <11.11%> (-0.04%) ⬇️
plugins-handlebars_hapi_hono 58.05% <11.11%> (-0.03%) ⬇️
plugins-ioredis_knex_langgraph 56.50% <11.11%> (-0.03%) ⬇️
plugins-ldapjs_light-my-request_limitd-client 57.75% <11.11%> (-0.03%) ⬇️
plugins-lodash_mariadb_memcached 57.45% <11.11%> (-0.03%) ⬇️
plugins-moleculer_mongodb_mongodb-core 60.89% <11.11%> (-0.03%) ⬇️
plugins-mongoose_multer_mysql 58.73% <11.11%> (-0.03%) ⬇️
plugins-mysql2_nats_node-serialize 60.35% <11.11%> (-0.03%) ⬇️
plugins-opensearch_passport-http_pino 58.60% <11.11%> (-0.03%) ⬇️
plugins-postgres_process_pug 57.75% <11.11%> (-0.03%) ⬇️
plugins-redis_router_sequelize 60.97% <15.00%> (-0.03%) ⬇️
plugins-test-and-upstream-rhea_undici_url 60.54% <10.00%> (-0.04%) ⬇️
plugins-valkey_vm_winston 57.18% <11.11%> (-0.03%) ⬇️
plugins-ws 58.73% <11.11%> (-0.03%) ⬇️
profiling 61.15% <21.73%> (-0.04%) ⬇️
serverless-aws-sdk-aws-sdk 54.40% <13.04%> (-0.03%) ⬇️
serverless-aws-sdk-base-inject-field 50.47% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-bedrockruntime 54.18% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-client 55.70% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-dynamodb 55.01% <11.11%> (-0.02%) ⬇️
serverless-aws-sdk-eventbridge 56.48% <13.04%> (-0.03%) ⬇️
serverless-aws-sdk-kinesis 58.54% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-lambda 56.72% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-s3 55.10% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-serverless-peer-service 59.12% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-sns 59.33% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-sqs 59.74% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-stepfunctions 54.93% <11.11%> (-0.03%) ⬇️
serverless-aws-sdk-util 50.99% <11.11%> (-0.04%) ⬇️
serverless-bucket-0 53.34% <10.52%> (-0.01%) ⬇️
serverless-bucket-1 58.35% <21.05%> (-0.03%) ⬇️
test-optimization-cucumber 70.55% <46.87%> (-0.03%) ⬇️
test-optimization-cypress 64.75% <51.61%> (+0.04%) ⬆️
test-optimization-jest 72.11% <63.41%> (+0.12%) ⬆️
test-optimization-mocha 72.05% <57.14%> (+0.04%) ⬆️
test-optimization-playwright-playwright-atr 59.52% <18.51%> (-0.02%) ⬇️
test-optimization-playwright-playwright-efd 59.67% <18.51%> (-0.02%) ⬇️
test-optimization-playwright-playwright-final-status 59.83% <18.51%> (-0.02%) ⬇️
test-optimization-playwright-playwright-impacted-tests 59.39% <29.62%> (+0.13%) ⬆️
test-optimization-playwright-playwright-reporting 60.86% <18.51%> (-0.07%) ⬇️
test-optimization-playwright-playwright-test-management 60.32% <18.51%> (-0.11%) ⬇️
test-optimization-playwright-playwright-test-span 59.56% <18.51%> (-0.08%) ⬇️
test-optimization-selenium 58.78% <34.09%> (-0.15%) ⬇️
test-optimization-testopt 57.42% <52.38%> (+0.05%) ⬆️
test-optimization-vitest 72.71% <78.57%> (+0.03%) ⬆️
test-optimization-vitest-browser 58.62% <18.51%> (-0.03%) ⬇️
test-optimization-webdriverio 64.91% <25.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pr-commenter

pr-commenter Bot commented Jul 27, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-17 10:58:58

Comparing candidate commit 3ee4385 in PR branch BridgeAR/2026-07-27-eslint-object-keys-length with baseline commit b85f911 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2283 metrics, 9 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-iast-with-vulnerability-iast-enabled-default-config-20

  • unstable max_rss_usage [-29.799MB; +11.176MB] or [-7.812%; +2.930%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-224.465ms; +439.364ms] or [-4.642%; +9.085%]
  • unstable execution_time [-225.326ms; +438.863ms] or [-4.588%; +8.935%]
  • unstable throughput [-156375.193op/s; +79709.475op/s] or [-9.146%; +4.662%]

scenario:plugin-aws-sdk-lambda-inject-with-context-24

  • unstable execution_time [-182.411ms; +205.913ms] or [-4.706%; +5.312%]

scenario:plugin-claude-agent-sdk-compact-stream-scan-26

  • unstable cpu_usage_percentage [-6.016%; +4.347%]

scenario:plugin-graphql-long-with-depth-and-collapse-off-20

  • unstable max_rss_usage [-15.461MB; +26.647MB] or [-3.950%; +6.808%]

scenario:plugin-graphql-long-with-depth-off-20

  • unstable max_rss_usage [-7386.469KB; +8524.755KB] or [-5.692%; +6.570%]

scenario:test-optimization-large-suite-20

  • unstable max_rss_usage [-5180.331KB; +5424.997KB] or [-6.454%; +6.758%]

@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-07-27-eslint-object-keys-length branch 2 times, most recently from 8c67d50 to 7579490 Compare August 12, 2026 14:48
@BridgeAR
BridgeAR marked this pull request as ready for review August 12, 2026 16:09
@BridgeAR
BridgeAR requested review from a team as code owners August 12, 2026 16:09
@BridgeAR
BridgeAR requested review from khanayan123 and wconti27 and removed request for a team August 12, 2026 16:09

@datadog-datadog-us1-prod datadog-datadog-us1-prod Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

More details

Repeated and partial require-hook teardown preserved remaining registrations, while absent coverage, empty baggage, profiler metadata, and conditional OpenAI log payloads retained their expected output contracts.

Was this helpful? React 👍 or 👎

📊 Validated against 6 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit e2ca12b · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

IlyasShabi
IlyasShabi previously approved these changes Aug 17, 2026
…e's shape

`parseSkippableSuitesResponse` leaves `coverage` undefined when the backend
sends none, and the instrumentations keep that `undefined` instead of
re-normalizing it to `{}`. Reinstating an empty-object default anywhere along
that path turns "nothing to backfill" back into a state only an `Object.keys`
probe can tell apart from real coverage.

`getConfiguredEfdRetryCount` relies on `getMaxEfdRetryCount` already returning
`undefined` for an empty bucket map, so the caller-side emptiness check folds
into the existing fallback.
…o log

The extraction helpers return a store instead of filling one the caller
allocated up front, so `store.openai` stays undefined for the methods that
record nothing. `sendLog` keys off that: any helper that starts writing to the
store must create it, or its data never reaches the log.
`SpaceProfiler` has nothing to contribute, so it returns undefined rather than
an empty object the submitter has to probe before dropping it.

`webTags` is only ever the tag map of a span `isWebServerSpan` matched, so it
always carries `span.type`; the emptiness check in front of the endpoint label
could never fire.
… aiguard

1. `ritm` counts hooked modules as they are registered and removed. The counter
   is the only thing deciding whether teardown resets the require patch, so
   every add and delete of a `moduleHooks` key has to keep it in step; `unhook`
   skips modules it no longer holds so a repeated teardown cannot undercount.
2. The OTel context manager builds the baggage entries in one pass and lets the
   absence of that object stand for "no baggage".
3. Azure Functions span links drop the probe in front of `tracer.extract`,
   which already returns null for a carrier that holds no trace context.
…*/src

The selector only matches probe positions — a comparison against 0, a `!`
operand, or the test of an `if`, ternary, logical expression or loop — so a
genuine count such as `writeMapPrefix(Object.keys(x).length)` stays legal.

Four sites keep the probe behind an inline opt-out. Fastify cookies and
bodies and router path params reach us as third-party objects with arbitrary
keys, and publishing an empty one would set the matching WAF address and burn
the once-per-request publish latch. The debugger snapshot walker faces the same
shape: `fields` is keyed by the user's variable names.
Pin the undefined values returned when profiler info or suite coverage is absent.

Exercise legacy OpenAI edit logging through the public SDK path so the lazy store branch remains covered.
Loading nock at module scope patches HTTP before agent.load(), so OpenAI instrumentation wraps the patched methods and the remaining span assertions fail.
On Node.js 24 with representative small configs, spreading and probing takes 10.3–11.5 ns versus 17.5–20.9 ns for copying entries while tracking state.
Leaving Nock active after the legacy edit request rejects every subsequent VCR call as an unmatched request.
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-07-27-eslint-object-keys-length branch from e2ca12b to 3ee4385 Compare August 17, 2026 10:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ee43851e3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread eslint.config.mjs
@BridgeAR
BridgeAR merged commit f3e5e1a into master Aug 17, 2026
1345 of 1839 checks passed
@BridgeAR
BridgeAR deleted the BridgeAR/2026-07-27-eslint-object-keys-length branch August 17, 2026 12:59
dd-octo-sts Bot pushed a commit that referenced this pull request Aug 17, 2026
`Object.keys(obj).length` allocates a full key array when callers only need to know whether data exists. Replace those probes with producer-owned presence state, `undefined` for absent results, and existing API absence signals, preserving the distinction between absent and populated data without a consumer-side allocation.

A narrow `no-restricted-syntax` rule catches the pattern only in boolean/test positions, leaving genuine counts valid. Explicit exceptions remain for arbitrary user-defined keys and copy paths where separate presence tracking would change existing semantics.
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Aug 17, 2026
dd-octo-sts Bot pushed a commit that referenced this pull request Aug 17, 2026
`Object.keys(obj).length` allocates a full key array when callers only need to know whether data exists. Replace those probes with producer-owned presence state, `undefined` for absent results, and existing API absence signals, preserving the distinction between absent and populated data without a consumer-side allocation.

A narrow `no-restricted-syntax` rule catches the pattern only in boolean/test positions, leaving genuine counts valid. Explicit exceptions remain for arbitrary user-defined keys and copy paths where separate presence tracking would change existing semantics.
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Aug 17, 2026
pabloerhard pushed a commit that referenced this pull request Aug 17, 2026
`Object.keys(obj).length` allocates a full key array when callers only need to know whether data exists. Replace those probes with producer-owned presence state, `undefined` for absent results, and existing API absence signals, preserving the distinction between absent and populated data without a consumer-side allocation.

A narrow `no-restricted-syntax` rule catches the pattern only in boolean/test positions, leaving genuine counts valid. Explicit exceptions remain for arbitrary user-defined keys and copy paths where separate presence tracking would change existing semantics.
pabloerhard pushed a commit that referenced this pull request Aug 17, 2026
`Object.keys(obj).length` allocates a full key array when callers only need to know whether data exists. Replace those probes with producer-owned presence state, `undefined` for absent results, and existing API absence signals, preserving the distinction between absent and populated data without a consumer-side allocation.

A narrow `no-restricted-syntax` rule catches the pattern only in boolean/test positions, leaving genuine counts valid. Explicit exceptions remain for arbitrary user-defined keys and copy paths where separate presence tracking would change existing semantics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants